﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using CustomORM.DataAccess;
using CustomORM.Extensions;
using CustomORM.Repository;

namespace CustomORM.Mapping
{
    public interface IEntityToTableMapper
    {
        RecordData GetRecordData(object model);
        void SetKeyValue(object model, object newValue);
        void SetValueForColumn(object model, string columnName, object newValue);
        List<ChildrenListWithMetadata> GetRelatedChildrenList(object modelObjectObject);
        string GenerateSimpleSelectSQL(string columnName, object columnValue);
        string GenerateSimpleSelectWhereKey(object keyValue);
        string GenerateSimpleSelectSQL();
        object GetKeyValue(object model);
        string TableName { get; set; }
        string KeyFieldName { get; set; }
        string KeyFieldNameUnique { get; set; }
        List<string> GetFieldNames();
        List<string> GetFieldNamesUnique();
        List<string> GetFieldNamesForSql();
        List<string> GetFieldNamesUniqueForSql();
        IList MapToEntityList(DataTable dataTable);
        object SimpleMapToEntity(DataRow row);
        void MapReferencesAndChildrenToEntity(object entity, List<RowEntity> objectsRow);
        List<ReferenceWithMetadata> GetRelatedReferencesList(object modelObject);
        void SetReferenceValueForColumn(object modelObject, string columnName, object newValue);
    }

    public abstract class EntityToTableMapper<T> : IEntityToTableMapper where T : new()
    {
        #region MAPPING
        protected void Table(string tableName)
        {
            if (!string.IsNullOrEmpty(TableName))
            {
                throw new Exception(string.Format("Method Table() can be called only once to initialize mapper. Table name: {0}", tableName));
            }
            TableName = tableName;
            ID = MagicMapper.AddMapper(typeof(T), this);
        }

        protected MappingExpression<T> Id(Expression<Func<T, object>> expression, string columnName)
        {
            KeyFieldName = columnName;
            KeyFieldNameUnique = GetUniqueNameForField(columnName, 0);
            var mappingExpression = new MappingExpression<T>(expression, columnName);
            _keysDictionary.Add(mappingExpression);
            return mappingExpression;
        }

        protected MappingExpression<T> Map(Expression<Func<T, object>> expression, string columnName)
        {
            var mappingExpression = new MappingExpression<T>(expression, columnName);
            _fieldsDictionary.Add(mappingExpression);
            return mappingExpression;
        }

        protected MappingExpression<T> References(Expression<Func<T, object>> expression, string columnName)
        {
            var mappingExpression = new MappingExpression<T>(expression, columnName);
            _referencesDictionary.Add(mappingExpression);
            return mappingExpression;
        }

        protected MappingExpression<T> HasMany(Expression<Func<T, object>> expression, string foreighnKeyName)
        {
            var mappingExpression = new MappingExpression<T>(expression, foreighnKeyName);
            _childrenDictionary.Add(mappingExpression);
            return mappingExpression;
        }

        #endregion MAPPING

        #region SETTERS

        public void SetKeyValue(object modelObject, object newValue)
        {
            var model = GetTypedModelFromObject(modelObject);

            if (_keysDictionary.Count != 1)
            {
                throw new Exception("only one key is supported");
            }
            var expression = _keysDictionary.First();

            SetValueUsingExpression(expression, model, newValue);
        }

        public void SetReferenceValueForColumn(object modelObject, string columnName, object newValue)
        {
            var model = GetTypedModelFromObject(modelObject);
            var mappingExpression = _referencesDictionary.FirstOrDefault(x => x.ColumnName == columnName);
            if (mappingExpression == null)
            {
                throw new Exception(string.Format("there is no such column as: {0}", columnName));
            }
            SetValueUsingExpression(mappingExpression, model, newValue);
        }

        public void SetValueForColumn(object modelObject, string columnName, object newValue)
        {
            var model = GetTypedModelFromObject(modelObject);

            var mappingExpression = _keysDictionary.FirstOrDefault(x => x.ColumnName == columnName);
            if (mappingExpression == null)
            {
                mappingExpression = _fieldsDictionary.FirstOrDefault(x => x.ColumnName == columnName);
            }
            if (mappingExpression == null)
            {
                mappingExpression = _referencesDictionary.FirstOrDefault(x => x.ColumnName == columnName);
                // for now not handling referenced entities...
                return;
            }
            if (mappingExpression == null)
            {
                throw new Exception(string.Format("there is no such column as: {0}", columnName));
            }

            SetValueUsingExpression(mappingExpression, model, newValue);
        }

        protected void SetValueUsingExpression(MappingExpression<T> mappingExpression, T model, object newValue)
        {
            var expressionBody = mappingExpression.ExecExpression.Body;
            MemberExpression member;

            if (expressionBody.NodeType == ExpressionType.MemberAccess)
            {
                member = (MemberExpression)expressionBody;
            }
            else if (expressionBody.NodeType == ExpressionType.Convert)
            {
                member = (MemberExpression)((UnaryExpression)expressionBody).Operand;
            }
            else
            {
                throw new Exception(string.Format("Following expression is not currently supported in mapping: {0}", expressionBody));
            }

            newValue = PrepareNewValue(mappingExpression, model, newValue, member.Type);

            MethodInfo setMethod = ((PropertyInfo)member.Member).GetSetMethod();
            setMethod.Invoke(model, new object[] { newValue });
        }

        private static object PrepareNewValue(MappingExpression<T> mappingExpression, T model, object newValue, Type destinationType)
        {
            // if newValue is DbNull convert it to null
            newValue = DbConversionsHelper.GetNullableValueFromDbValue(newValue);
            if (mappingExpression.HasFieldConverter)
            {
                newValue = mappingExpression.FieldConverter.ConvertFromDb(model, newValue);
            }
            if (newValue != null && destinationType != newValue.GetType())
            {
                var incomingType = newValue.GetType();
                // Add other mismatch conversions...
                if (incomingType == typeof(byte[]) && destinationType == typeof(string))
                {
                    var decodedString = Encoding.UTF8.GetString((byte[])newValue);
                    return decodedString;
                }
                if (incomingType == typeof(decimal) && destinationType == typeof(int))
                {
                    return Convert.ToInt32(newValue);
                }
                throw new Exception(string.Format("Cannot convert from {0} to {1}", incomingType, destinationType));
            }
            return newValue;
        }

        #endregion SETTERS

        public List<ReferenceWithMetadata> GetRelatedReferencesList(object modelObject)
        {
            var model = GetTypedModelFromObject(modelObject);
            var result = new List<ReferenceWithMetadata>();
            foreach (var expression in _referencesDictionary)
            {
                var reference = expression.FuncReady.Invoke(model);
                if (reference != null)
                {
                    var referenceWithMetadata = new ReferenceWithMetadata() { Reference = reference, MappingExpression = expression };
                    result.Add(referenceWithMetadata);
                }
            }
            return result;
        }

        public List<ChildrenListWithMetadata> GetRelatedChildrenList(object modelObject)
        {
            var model = GetTypedModelFromObject(modelObject);

            var result = new List<ChildrenListWithMetadata>();

            foreach (var expression in _childrenDictionary)
            {
                var childrenOfOneType = (IList)expression.FuncReady.Invoke(model);
                //var children = new List<object>();
                if (childrenOfOneType != null && childrenOfOneType.Count > 0)
                {
                    var childrenListWithMetadata = new ChildrenListWithMetadata() { Children = childrenOfOneType, MappingExpression = expression };
                    result.Add(childrenListWithMetadata);
                }
            }
            return result;
        }

        public RecordData GetRecordData(object modelObject)
        {
            var model = GetTypedModelFromObject(modelObject);

            var keyFieldAssigned = false;
            var keyNames = new List<object>();
            var keyValues = new List<object>();
            var fieldNames = new List<object>();
            var fieldValues = new List<object>();

            // Have these foreach-es separate as it may involve more logic in future
            foreach (var pair in _keysDictionary)
            {
                keyNames.Add(pair.ColumnName);
                fieldNames.Add(pair.ColumnName);

                var keyValue = PrepareDbObject(pair.FuncReady.Invoke(model));
                keyFieldAssigned = pair.AssignedValueToBeUsed;
                keyValues.Add(keyValue);
                fieldValues.Add(keyValue);
            }

            foreach (var pair in _fieldsDictionary)
            {
                fieldNames.Add(pair.ColumnName);
                fieldValues.Add(PrepareDbObject(pair.FuncReady.Invoke(model)));
            }

            foreach (var pair in _referencesDictionary)
            {
                fieldNames.Add(pair.ColumnName);
                var referencedEntity = pair.FuncReady.Invoke(model);
                if (referencedEntity == null)
                {
                    fieldValues.Add(PrepareDbObject(null));
                }
                else
                {
                    var data = MagicMapper.GetRecordData(referencedEntity);
                    // If reference is done like .References(x => Something, "COLUMN_1,COLUMN_2")
                    // than Something will have composite key in map Id(x => Id1, "COLUMN_1");Id(x => Id2, "COLUMN_2")
                    // thus we have to add range of key values!!!
                    fieldValues.AddRange(data.KeyValues.Select(PrepareDbObject));
                }
            }

            var recordData = new RecordData()
                                 {
                                     TableName = TableName,
                                     FieldNames = fieldNames.ToArray(),
                                     FieldValues = fieldValues.ToArray(),
                                     KeyNames = keyNames.ToArray(),
                                     KeyValues = keyValues.ToArray(),
                                     KeyFieldAssigned = keyFieldAssigned
                                 };

            return recordData;
        }

        public object GetKeyValue(object modelObject)
        {
            var model = GetTypedModelFromObject(modelObject);

            if (_keysDictionary == null || _keysDictionary.Count != 1)
            {
                throw new InvalidOperationException("GetKeyValue (for now) can be used only for entities with one key value.");
            }
            var keyValue = _keysDictionary.First().FuncReady.Invoke(model);

            return keyValue;
        }

        #region Field Names

        public List<string> GetFieldNames()
        {
            if (FieldNames != null)
            {
                // return cached FieldNames
                return FieldNames;
            }
            else
            {
                var fieldNames = GetRawFieldNames();

                // caching FieldNames for further usages, they are not intended to be chagned
                FieldNames = fieldNames;
            }

            return FieldNames;
        }

        private List<string> GetRawFieldNames()
        {
            var fieldNames = _keysDictionary.Select(pair => pair.ColumnName).ToList();
            fieldNames.AddRange(_fieldsDictionary.Select(pair => pair.ColumnName));
            fieldNames.AddRange(_referencesDictionary.Select(pair => pair.ColumnName));
            return fieldNames;
        }

        public List<string> GetFieldNamesUnique()
        {
            if (FieldNamesUnique != null)
            {
                return FieldNamesUnique;
            }
            else
            {
                var fieldNames = GetRawFieldNames();
                FieldNamesUnique = new List<string>();
                for (int i = 0; i < fieldNames.Count; i++)
                {
                    var uniqueName = GetUniqueNameForField(fieldNames[i], i);
                    FieldNamesUnique.Add(uniqueName);
                }

            }
            return FieldNamesUnique;
        }

        private string GetUniqueNameForField(string fieldName, int fieldNumber)
        {
            // this ensures that we have FieldNamesUnique similar to _9CustomerID, as it will be used in aliases
            string valueToAdd = fieldName;
            var maxLength = 24;
            if (fieldName.Length > maxLength)
            {
                valueToAdd = string.Format("{0}{1}", fieldName.Remove(maxLength, fieldName.Length - maxLength), fieldNumber);
            }
            var uniqueName = string.Format("_{0}{1}", ID, valueToAdd);
            return uniqueName;
        }

        public List<string> FieldNamesForSql { get; private set; }

        public List<string> GetFieldNamesForSql()
        {
            if (FieldNamesForSql != null)
            {
                return FieldNamesForSql;
            }
            else
            {
                var fieldNames = new List<string>();
                foreach (var fieldName in GetFieldNames())
                {
                    if (fieldName.Contains(' '))
                    {
                        fieldNames.Add(string.Format("[{0}]", fieldName));
                    }
                    else
                    {
                        fieldNames.Add(fieldName);
                    }
                }

                // this ensures that we have FieldNamesUnique similar to _9CustomerID, as it will be used in aliases
                FieldNamesForSql = fieldNames;
            }
            return FieldNamesForSql;
        }

        public List<string> FieldNamesUniqueForSql { get; private set; }

        public List<string> GetFieldNamesUniqueForSql()
        {
            if (FieldNamesUniqueForSql != null)
            {
                return FieldNamesUniqueForSql;
            }
            else
            {
                var fieldNames = new List<string>();
                foreach (var fieldName in GetFieldNamesUnique())
                {
                    if (fieldName.Contains(' '))
                    {
                        fieldNames.Add(string.Format("[{0}]", fieldName));
                    }
                    else
                    {
                        fieldNames.Add(fieldName);
                    }
                }

                // this ensures that we have FieldNamesUnique similar to _9CustomerID, as it will be used in aliases
                FieldNamesUniqueForSql = fieldNames;
            }
            return FieldNamesUniqueForSql;
        }

        #endregion



        protected T GetTypedModelFromObject(object modelObject)
        {
            if (!(modelObject is T))
            {
                throw new Exception(string.Format("Ensure that {0} is existing mapping and that it is registered.", modelObject.GetType()));
            }
            return (T)modelObject;
        }

        // Instead of running this method on all values, we can add IsNullable to the mapping files
        protected static object PrepareDbObject(object value)
        {
            return value ?? DBNull.Value;
        }

        #region Properties
        private List<MappingExpression<T>> _keysDictionary = new List<MappingExpression<T>>();
        public List<MappingExpression<T>> KeysDictionary
        {
            get { return _keysDictionary; }
            private set { _keysDictionary = value; }
        }

        private List<MappingExpression<T>> _fieldsDictionary = new List<MappingExpression<T>>();
        public List<MappingExpression<T>> FieldsDictionary
        {
            get { return _fieldsDictionary; }
            private set { _fieldsDictionary = value; }
        }

        private List<MappingExpression<T>> _referencesDictionary = new List<MappingExpression<T>>();
        public List<MappingExpression<T>> ReferencesDictionary
        {
            get { return _referencesDictionary; }
            private set { _referencesDictionary = value; }
        }

        private List<MappingExpression<T>> _childrenDictionary = new List<MappingExpression<T>>();

        public List<MappingExpression<T>> ChildrenDictionary
        {
            get { return _childrenDictionary; }
            private set { _childrenDictionary = value; }
        }
        #endregion

        public string TableName { get; set; }
        public string KeyFieldName { get; set; }
        public string KeyFieldNameUnique { get; set; }
        protected string ID { get; private set; }
        protected List<string> FieldNames { get; private set; }
        protected List<string> FieldNamesUnique { get; private set; }

        public string GenerateSimpleSelectSQL()
        {
            List<string> fieldNames = GetFieldNames();
            string fieldNamesString = SqlBuilder.CommaSeparate(fieldNames);
            return string.Format("SELECT {0} FROM {1}", fieldNamesString, TableName);
        }

        public string GenerateSimpleSelectSQL(string columnName, object columnValue)
        {
            List<string> fieldNames = GetFieldNames();
            string fieldNamesString = SqlBuilder.CommaSeparate(fieldNames);
            object preparedValue = PrepareDbObject(columnValue);

            return string.Format("SELECT {0} FROM {1} WHERE {2} = {3}", fieldNamesString, TableName, columnName, preparedValue);
        }

        public string GenerateSimpleSelectWhereKey(object keyValue)
        {
            if (keyValue == null)
            {
                throw new Exception("You cannot generate SQL with NULL key value.");
            }
            if (_keysDictionary.Count > 1)
            {
                throw new Exception("No support for SELECT when table has more than one key.");
            }
            string keyName = _keysDictionary.First().ColumnName;
            return GenerateSimpleSelectSQL(keyName, keyValue);
        }

        /// <summary>
        /// Simplified mapping to the list of entities... this is only for select from one table
        /// </summary>
        /// <param name="dataTable"></param>
        /// <returns></returns>
        public IList MapToEntityList(DataTable dataTable)
        {
            var result = new List<T>();

            List<string> fieldNames = GetFieldNames();

            foreach (var row in dataTable.Rows)
            {
                T entity = MapToEntity((DataRow)row, fieldNames);
                result.Add(entity);
            }

            return result;
        }

        /// <summary>
        /// Simplified mapping... only for one select from one table
        /// </summary>
        /// <param name="dataRow"></param>
        /// <param name="fieldNames"></param>
        /// <returns></returns>
        public T MapToEntity(DataRow dataRow, List<string> fieldNames)
        {
            var newEntity = new T();

            foreach (var fieldName in fieldNames)
            {
                object newValue = dataRow[fieldName];
                SetValueForColumn(newEntity, fieldName, newValue);
            }

            return newEntity;
        }

        #region simple mapping

        public object SimpleMapToEntity(DataRow row)
        {
            var fieldNames = GetFieldNames();
            var fieldNamesUnique = GetFieldNamesUnique();
            return MapToEntity(row, fieldNames, fieldNamesUnique);
        }

        #endregion


        #region From table to entities
        public T MapToEntity(DataRow dataRow, List<string> fieldNames, List<string> fieldNamesWithAlias)
        {
            var newEntity = new T();

            for (int i = 0; i < fieldNames.Count; i++)
            {
                object newValue = dataRow[fieldNamesWithAlias[i]];
                SetValueForColumn(newEntity, fieldNames[i], newValue);
            }

            return newEntity;
        }

        public void MapReferencesAndChildrenToEntity(object entityObject, List<RowEntity> objectsRow)
        {
            if (entityObject == null) return;

            var entity = (T)entityObject;
            var parentRowEntity = objectsRow.Where(x => x.Participant.Type == typeof (T)).First();

            foreach (var expression in ReferencesDictionary)
            {
                Type typeOfReferencedEntity = expression.ExecExpression.Body.Type;
                var refRowEntity = objectsRow.Where(x => x.Participant.Type == typeOfReferencedEntity).FirstOrDefault();

                if (refRowEntity == null)
                {
                    // nothing to do for this reference entity
                    continue;
                }

                // If there is change in parent object or in reference object we have to glue them together
                if (parentRowEntity.HasChanged || refRowEntity.HasChanged)
                {
                    SetValueUsingExpression(expression, entity, refRowEntity.EntityValue);
                }

                // TODO: ATTENTION (!) possible stackoverflow... work on this in future
                refRowEntity.Participant.Mapper.MapReferencesAndChildrenToEntity(refRowEntity.EntityValue, objectsRow);
            }

            foreach (var mappingExpression in ChildrenDictionary)
            {
                var childrenListType = mappingExpression.ExecExpression.Body.Type;
                if (!childrenListType.IsGenericType)
                {
                    throw new Exception("As for now non-generic children are not supported");
                }
                var childrenType = childrenListType.GetGenericArguments()[0];

                var childrenRowEntity = objectsRow.Where(x => x.Participant.Type == childrenType).FirstOrDefault();

                if (childrenRowEntity == null)
                {
                    // nothing to do for this children entity
                    continue;
                }

                //continue only if criteria whants this child
                if (childrenRowEntity.HasChanged)
                {
                    // Getting list of children...
                    var childrenList = mappingExpression.FuncReady.Invoke(entity);
                    if (childrenList == null)
                    {
                        var listRef = typeof(List<>);
                        childrenList = Activator.CreateInstance(listRef.MakeGenericType(new[] { childrenType }));
                        SetValueUsingExpression(mappingExpression, entity, childrenList);
                    }
                    var add = typeof(IList).GetMethod("Add");
                    add.Invoke(childrenList, new object[] { childrenRowEntity.EntityValue });
                }

                // TODO: ATTENTION (!) possible stackoverflow... work on this in future
                childrenRowEntity.Participant.Mapper.MapReferencesAndChildrenToEntity(childrenRowEntity.EntityValue, objectsRow);
            }
        }
        #endregion

    }
}